package clutils;

use warnings;

BEGIN {
	use Net::Ping;
	use Data::Dumper;
	use Sys::Hostname;
	use cldb;
	use daemon_connect;
	use clstatdb;
	use clreq;
	use clreqdxml;
	use File::Basename;
	use Exporter ();
	our ($VERSION, @ISA, @EXPORT, @EXPORT_OK);

	$VERSION=0.6;
	@ISA	= qw(Exporter);
	@EXPORT	= qw(&availnodes &nextnode &map_select_output &to_bool &get_clreqdxml_object &get_clreqd_status &ping_connections &get_nodes_in_cluster);
}
our @EXPORT_OK = qw(get_master_node);

sub to_bool($) {
my $i=$_[0];

	return(0) if !defined($i);
	if($i =~ /^\d+$/) {
		$i=int($i);
		return 0 if $i==0;
		return 1;
	}
	$i=lc($i);
	if($i eq "y" || $i eq "yes") {
		return 1;
	}
	return 0;
}

#########################################################################
# availnodes(appname)							#
# ===================							#
# Return the list of nodes that are available to run the specified	#
# application. This takes into account the list of nodes that are	#
# running in the cluster, the nodes configured to run the application,	#
# and the modded_setttings table entries, if appropriate.		#
#									#
# If the cluster global variable topology_check is turned on it will	#
# also query the nodes in question to ensure they have the topologies	#
# required to run the application, and also that the topologies are	#
# working as expected.							#
#########################################################################

sub availnodes($) {
my $db;
my $h;
my @config_nodes;
my @up_nodes;
my @modded_settings_nodes;

	#################################################################
	# Get the list of nodes defined for the application.		#
	#################################################################
	my $app=$_[0];
	$db=new cldb("$::ENV{TRUECL_CFG}/config.db");
	if(!defined($db)) {
		return (0,"Unable to open configuration database.");
	}
	$h=$db->get_rows("SELECT nodes FROM applications WHERE name = '$app';");
	my $topology_check=0;
	my $ch2=$db->get_rows("SELECT * FROM globals WHERE var = 'topology_check';");
	my $ch3=$db->get_rows("SELECT * FROM application_ips WHERE name = '$app';");
	$db->disconnect; $db=undef;
	if(!defined($h) || !defined($h->[0])) {
		return(0,"nodes not defined for application '$app' in config database.");
	}
	if(defined($ch2) && defined($ch2->[0]->{value})) {
		$topology_check=to_bool($ch2->[0]->{value});
	}

	@config_nodes=split(/,/,$h->[0]->{nodes});

	#################################################################
	# Get the list of nodes that are currently running...		#
	#################################################################

	$db=new clstatdb("$::ENV{TRUECL_CFG}/status.db");
	if(!defined($db)) {
		return (0,"Unable to open status database.");
	}
	$db->create_if_needed("modded_settings");
	$h=$db->get_rows("SELECT * FROM cluster_status_vars WHERE value = 'Joined';");
	my $h2=$db->get_rows("SELECT * FROM modded_settings WHERE appname = '$app' AND attrname = 'nodelist';");
	$db->disconnect; $db=undef;
	if(!defined($h) || !defined($h->[0])) {
		return(0,"No results for running nodes in cluster_status_vars table.");
	}
	@up_nodes=();
	for (@$h) {
		if($_->{var} =~ /(.*):clusternodestate$/) {
			push @up_nodes,$1;
		}
	}

	#################################################################
	# Look for list of modded_settings entries...			#
	#################################################################
	@modded_settings_nodes=();
	if(defined($h2) && defined($h2->[0]->{value})) {
		@modded_settings_nodes=split(/,/,$h2->[0]->{value});
		my @n=();my $x;
		for $x (@modded_settings_nodes) {
			if(grep {$x eq $_} @up_nodes) {
				push @n,$x;
			}
		}
		@modded_settings_nodes=@n;
	}
	{
		my @n=();my $x;
		for $x (@config_nodes) {
			if(grep {$x eq $_} @up_nodes) {
				push @n,$x;
			}
		}
		@config_nodes=@n;
	}

	#################################################################
	# If topology check is set then we need to query the status	#
	# daemon on the node in question, [raising a warning if it is	#
	# not available], for all nodes in the list...			#
	#################################################################

	my @clist=@config_nodes;
	if(@modded_settings_nodes) {
		@clist=@modded_settings_nodes;
	}
	if($topology_check && defined($ch3) && defined($ch3->[0]->{network})) {
		my %app_tops=();
		for my $crec (@$ch3) {
			$app_tops{$crec->{network}}=1;
		}
		my %top_info=();
		my $clreqdxml=new clreqdxml("$::ENV{TRUECL_CFG}/clreqd.xml");
		if(! defined($clreqdxml)) {
			::error("Unable to load Request Daemon configuration info.", "Reason: " . clreqdxml::error());
			return(0,"Unable to load Request Daemon configuration info.");
		}
		for my $cnode (@clist) {
			my $reqd=new clreq(hosts => [$cnode], clreqdxml => $clreqdxml, timeout => 3);
			my $results=$reqd->run(cmd => ["perform_adhoc_sql","DBTYPE","status","SQL","SELECT * FROM topology_status"]);
			if(!defined($results)) {
				::warning("No results returned from node '$cnode' - ignoring.");
				next;
			}
			my $r=map_select_output($results);
			$top_info{$cnode}={};
			for my $cxx (@{$r->{$cnode}}) {
				$top_info{$cnode}->{$cxx->{topology}}=$cxx->{alive};
			}
		}
		#########################################################
		# top_info is a list of the information from all nodes,	#
		# so check them to see if any are appropriate of not.	#
		#########################################################
		my @ok=();
		for my $cnode (@clist) {
			next if !exists($top_info{$cnode});
			my $bad=0;
			for my $ctop (keys %app_tops) {
				if(!exists($top_info{$cnode}->{$ctop})) {
					if(defined(&::warning)) {
						::warning("Node '$cnode' does not define a topology '$ctop'.");
					}
					$bad=1; last;
				}
				if($top_info{$cnode}->{$ctop}==0) {
					$bad=1; last;
				}
			}
			push @ok,$cnode if !$bad;
		}
		@clist=@ok;
	}
	return(1,[@clist])
}

#########################################################################
# This attempts to return the name of the next ode to use in the	#
# cluster. It will return "$$NONE" if no suitable nodes are currently	#
# available, or "$$NOFAIL" if modded_settings nextnode has been set to	#
# blank to stop fail-overs from occuring.				#
# Will return '$$NOT_RUNNING' if the application is not up.		#
# $$DEAD_NODE nextnode - if the next node specified is not up.		#
#									#
# Like the avilnodes it will also check the configuration and status	#
# of the topology for the nodes too...
#########################################################################

sub nextnode($) {
my $app=$_[0];

	#################################################################
	# Get the settings for the app from the config database.	#
	#################################################################

	my $db=new cldb("$::ENV{TRUECL_CFG}/config.db");
	if(!defined($db)) {
		return (0,"Unable to open configuration database.");
	}
	my $h=$db->get_rows("SELECT * FROM applications WHERE name = '$app';");
	$db->disconnect; $db=undef;
	if(!defined($h) || !defined($h->[0]->{name})) {
		return(undef,"Unknown application '$app'.");
	}

	#################################################################
	# We are interested in prefnode and nodes fields.		#
	#################################################################
	my $prefnode=$h->[0]->{prefnode};
	my $nodelist=$h->[0]->{nodes};
	if(!defined($prefnode)) {
		$prefnode='$$ROUND_ROBIN';
	}
	my @NL=();
	@NL=split(/,/,$nodelist) if defined($nodelist);

	#################################################################
	# See if we have a nextnode and nodelist setting is 		#
	# modded_settings if it exists.					#
	#################################################################
	$db=new clstatdb("$::ENV{TRUECL_CFG}/status.db");
	if(!defined($db)) {
		return (undef,"Unable to open status database.");
	}
	$db->create_if_needed("modded_settings");
	$h=$db->get_rows("SELECT * FROM cluster_status_vars WHERE value = 'Joined';");
	my @up_nodes=();

	# Up_nodes simply used to be a series of nodes that were up	#
	# in the cluster. Now that topology support is available, we	#
	# use the availnode routine if possible.
	my ($_x,$_x2)=availnodes($app);
	if ($_x==1) {
		@up_nodes=(@$_x2);
	} else {
		for (@$h) {
			if($_->{var} =~ /(.*):clusternodestate$/) {
				push @up_nodes,$1;
			}
		}
	}
	my $h2=$db->get_rows("SELECT * FROM modded_settings WHERE appname = '$app' AND (attrname = 'nodelist' OR attrname = 'nextnode');");
	my $h3=$db->get_rows("SELECT * FROM app_status WHERE name = '$app';");
	$db->disconnect; $db=undef;
	if(!defined($h) || !defined($h->[0])) {
		return(undef,"No results for running nodes in cluster_status_vars table.");
	}

	my $ms_nodelist=undef;
	my $ms_nextnode="invalid value";
	if(defined($h2)) {
		if(exists($h2->[0]->{attrname}) && defined($h2->[0]->{attrname}) &&
			$h2->[0]->{attrname} eq "nextnode") {
			$ms_nextnode=$h2->[0]->{value};
		}
		if(exists($h2->[1]->{attrname}) && defined($h2->[1]->{attrname}) &&
			$h2->[1]->{attrname} eq "nextnode") {
			$ms_nextnode=$h2->[1]->{value};
		}
		if(exists($h2->[0]->{attrname}) && defined($h2->[0]->{attrname}) &&
			$h2->[0]->{attrname} eq "nodelist") {
			$ms_nodelist=$h2->[0]->{value};
		}
		if(exists($h2->[1]->{attrname}) && defined($h2->[1]->{attrname}) &&
			$h2->[1]->{attrname} eq "nodelist") {
			$ms_nodelist=$h2->[1]->{value};
		}
	}
	#########################################################
	# Return if fail-over has been disallowed.		#
	#########################################################
	if(!defined($ms_nextnode) || $ms_nextnode eq "") {
		return ('$$NOFAIL',undef);
	}
	if(defined($ms_nextnode) && $ms_nextnode ne "invalid value") {
		#################################################
		# XXX: Show add stuff here for other than 	#
		#	round robin stuff.			#
		#################################################
		if(grep {$_ eq $ms_nextnode} @up_nodes) {
			return ($ms_nextnode,undef);
		}
		#################################################
		#  The next node specified is not up!		#
		#################################################
		return("\$\$DEAD_NODE $ms_nextnode",undef);
	}

	#########################################################
	# Get the current node - and abort if not running.	#
	#########################################################
	if($h3->[0]->{status} ne "RUNNING") {
		return('$$NOT_RUNNING',undef);
	}

	my $cnode=$h3->[0]->{host};
	my @N=();
	if(defined($ms_nodelist)) {
		$ms_nodelist=~ s/\s+//g;
		@N=split(/,/,$ms_nodelist);
	} else {
		$nodelist=~ s/\s+//g;
		@N=split(/,/,$nodelist);
	}

	#########################################################
	# @N is the list of nodes to check - we should remove	#
	# any entries not in up_list since those nodes are down.#
	#########################################################

	my @N2=();
	for my $cn (@N) {
		if(grep {$_ eq $cn} @up_nodes) {
			push @N2,$cn;
		}
	}

	@N=@N2;
	if(! @N) {
		return('$$NONE',undef);
	}

	my $c=0;
	my $f=0;
	while ($c<@N) {
		if($N[$c] eq $cnode) {
			$f=1; last;
		}
		$c++;
	}
	#########################################################
	# if the app has not been found simply return the 	#
	# first node in the list.				#
	#########################################################
	if(!$f) {
		return($N[0],undef);
	}
	$c++;
	if($c==@N) {
		#################################################################
		# Edge case - if there is just a single item in the list	#
		# then only one node, so no next node!				#
		#################################################################
		if(@N==1) {
			return('$$NONE',undef);
		}
		return($N[0],undef);
	}
	return ($N[$c],undef);
}

sub map_select_output($) {
my $r=$_[0];

	my %results=();
	my $chost;
	for $chost (keys(%$r)) {
		if(!exists($results{$chost})) {
			$results{$chost}=[];
		}
		my $l=$r->{$chost};
		if($l =~ /^(ERROR|WARNING)/) {
			$results{$chost}=[$l];
			next;
		}
		my @LI=split(/\n/,$l);
		my @f=split(/\|/,$LI[0]);
		my $cl;
		for $cl (@LI[1 .. $#LI]) {
			my %fields=();
			my $item=0;
			my @citem=split(/\|/,$cl);
			while($item<@citem) {
				$fields{$f[$item]}=$citem[$item];
				$item++;
			}
			push @{$results{$chost}},{%fields};
		}
	}
	return {%results};
}

#########################################################################
# Return two references - one for a list of alive nodes, and one for	#
# the list of dead nodes [well those not running clreqd].		#
#########################################################################

sub get_clreqd_status {
my $clreqdxml;
my $timeout=shift;

	$timeout=2 if !defined($timeout);
	$clreqdxml=new clreqdxml("$::ENV{TRUECL_CFG}/clreqd.xml");
	if(! defined($clreqdxml)) {
		error("Unable to load Request Daemon configuration info.", "Reason: " . clreqdxml::error(),1);
	}

	my $reqd=new clreq(hosts => [$clreqdxml->get_host_list], clreqdxml => $clreqdxml, timeout => $timeout);
	my $results=$reqd->run(cmd => ["echo"]);

	my @OK=(); my @DEAD=();
	for (keys(%$results)) {
		if($results->{$_} =~ /Alive!/) {
			push @OK,$_;
		} else {
			push @DEAD,$_;
		}
	}
	return([@OK],[@DEAD]);
}

sub get_clreqdxml_object {
	my $clreqdxml=new clreqdxml("$::ENV{TRUECL_CFG}/clreqd.xml");
	if(! defined($clreqdxml)) {
		error("Unable to load Request Daemon configuration info.", "Reason: " . clreqdxml::error(),1);
	}
	return $clreqdxml;
}

sub ping_connections {
my $cldb;
my $verbose=0;

	if(exists($_[0]) && defined($_[0])) {
		$verbose=shift;
	}

	my $db=new cldb("$::ENV{TRUECL_CFG}/config.db");
	if(!defined($db)) {
		return (2,"Unable to open configuration database.");
	}
	my $h=$db->get_rows("SELECT * FROM known_net_connections;");
	$db->disconnect; $db=undef;
	if(!defined($h) || !defined($h->[0]) || !defined($h->[0]->{ipaddr})) {
		return(2,"No known_net_connection details defined.");
	}

	#################################################################
	# We sort the list by timeout, choosing the shortest first.	#
	#################################################################

	my @rows=();
	if(scalar(@$h)>1) {
		@rows=sort {
			my $a2=$h->[$a]->{timeout};
			my $b2=$h->[$b]->{timeout};
			$a2 <=> $b2;
			} (0 .. scalar(@$h)-1);
	} else {
		@rows=(0);
	}

	# Now start ping'ing each in turn...

	my $tresult=0;
	my @results=();
	my $p = Net::Ping->new("icmp");
	for my $c_row (@rows) {
		if($verbose==0) {
			if($p->ping($h->[$c_row]->{ipaddr},$h->[$c_row]->{timeout})) {
				return(1,undef);
			}
		} else {
			if($p->ping($h->[$c_row]->{ipaddr},$h->[$c_row]->{timeout})) {
				$tresult=1;
				push @results,"$h->[$c_row]->{ipaddr} OK";
			} else {
				push @results,"$h->[$c_row]->{ipaddr} FAIL";
			}
		}
	}
	if($verbose) {
		return ($tresult,join("\n",@results));
	}
	return(0,"All pings failed");	# Nothing worked
}

#################################################################
# This will make use of the 'firstonly' option to return the	#
# name of the master node. This node can then be queried to	#
# understand the list of running nodes in the cluster using	#
# other requests.						#
#################################################################

sub get_master_node {
my $clreqdxml;
my $timeout=shift;

	$timeout=2 if !defined($timeout);
	$clreqdxml=new clreqdxml("$::ENV{TRUECL_CFG}/clreqd.xml");
	if(! defined($clreqdxml)) {
		error("Unable to load Request Daemon configuration info.", "Reason: " . clreqdxml::error(),1);
	}

	my $reqd=new clreq(hosts => [$clreqdxml->get_host_list], clreqdxml => $clreqdxml, timeout => $timeout);
	my $results=$reqd->run(cmd => ["get_master_node"], firstonly=>1);

	my @L=keys(%$results);
	my $rr=undef;
	for my $ckey (@L) {
		if(defined($results->{$ckey})) {
			$rr=$results->{$ckey}; chomp $rr;
			$rr =~ s/^.*\s+//;
			last;
		}
	}
	return $rr;
}

#########################################################################
# Get a list of nodes that are currently part of the formed cluster.	#
# If the master node is not specified, then we use the above		#
# get_master_node routine to return it.					#
# Results:								#
# 0,undef	- Could not determine master node [cluster down]	#
# 1,undef	- The clusterd on what is the master node is not	#
#		  running!						#
# 2,[nodes]	- cluster running - node 0 of the list is the master.	#
# 3,undef	- Unexpected internal error				#
# 4,undef	- Unable to query status database on master.		#
#########################################################################

sub get_nodes_in_cluster {
my $cmaster=shift;

	local $SIG{CHLD}='DEFAULT';
	if(!defined($cmaster)) {
		$cmaster=get_master_node();
		return(0,undef) if !defined($cmaster);
	}

	#################################################################
	# If we are the master query the local database directly to	#
	# save time!!!							#
	#################################################################
	if($cmaster eq hostname()) {
		my @N=();
		my $db=new clstatdb("$::ENV{TRUECL_CFG}/status.db");
		if(!defined($db)) {
			return (4,undef);
		}
		my $h=$db->get_rows("SELECT * FROM cluster_status_vars WHERE var LIKE '%:clusternodestate' AND value = 'Joined';");

		if(!defined($h) || !defined($h->[0]->{var})) {
			return(0,undef);
		}
		for my $crec (@$h) {
			my $x=$crec->{var};
			$x =~ s/:clusternodestate$//;
			push @N,$x;
		}
		return(2,[@N]);
	}

	#################################################################
	# Talk to the status daemon on this node to get the 		#
	# GET_CLSTATUS information.					#
	#################################################################
	my @N=($cmaster);
	my $statd=new daemon_connect(host => $cmaster,daemon => "statd",validate=>1,timeout=>5);
	if(defined($statd)) {
		my ($res,$msg)=$statd->send_request(MSG=>"GET_CLSTATUS");
		if($res!=0 || $msg =~ /NO_CLUSTERD/) {
			return(1,undef);
		}
		if($res==0) {
			my @L=split(/\n/,$msg);
			for my $cline (@L) {
				next if !length($cline);
				my ($a,$b)=split(/,/,$cline);
				if($b eq "Joined" && $a ne $cmaster) {
					push @N,$a;
				}
			}
			return(2,[@N]);
		}
	}

	#################################################################
	# If we get here the master node is available, but the		#
	# status daemon is not running on it. In this case we		#
	# query the status datasbase table cluster_status_vars		#
	# getting *:clusternodestate vars and values of Joined.		#
	#################################################################
	
	my $clreqdxml=new clreqdxml("$::ENV{TRUECL_CFG}/clreqd.xml");
	if(! defined($clreqdxml)) {
		return(3,undef); # internal error
	}

	my $reqd=new clreq(hosts => [$cmaster], clreqdxml => $clreqdxml, timeout => 20);
	if(!defined($reqd)) {
		return(3,undef); # internal error
	}
	my $results=$reqd->run(cmd => ["perform_adhoc_sql","DBTYPE","status","SQL",
		"SELECT * from cluster_status_vars WHERE var LIKE '%:clusternodestate';"]);
	if(!defined($results)) {
		return(4,undef); # Unable to query status database on master
	}
	my $r=map_select_output($results);
	for my $cxx (@{$r->{$cmaster}}) {
		if($cxx->{value} eq "Joined") {
			my ($a,$b)=split(/:/,$cxx->{var});
			push @N,$a if $a ne $cmaster;
		}
	}
	return(2,[@N]);
}

1;

